package org.redrock.test.repository;
import com.google.gson.Gson;
import org.redrock.test.bean.JsonWebToken;
import org.redrock.test.bean.JwtFormData;
import org.redrock.test.bean.UserAccessToken;
import org.redrock.test.exception.WeixinException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

@Repository
public class JwtRepository {
    private final Logger logger = LoggerFactory.getLogger(JwtRepository.class);

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 通过 token 获取 jwt
     * @param token
     * @return
     */
    public JsonWebToken getJwtByToken(String token) throws WeixinException {
        JsonWebToken result = jdbcTemplate.query(
                "select token, refresh_token, `timestamp`, secret from jwt where token = ?",
                preparedStatement -> {
                    preparedStatement.setString(1, token);
                },
                resultSet -> {
                    if (resultSet.next()) {
                        JsonWebToken jwt = new JsonWebToken();
                        jwt.setToken(resultSet.getString("token"));
                        jwt.setSecret(resultSet.getString("secret"));
                        jwt.setTimestamp(resultSet.getInt("timestamp"));
                        jwt.setRefreshToken(resultSet.getString("refresh_token"));
                        return jwt;
                    }
                    return null;
                }
        );
        if (result == null) throw new WeixinException(HttpStatus.NOT_FOUND, "token 失效");
        if (result.getTimestamp() < this.getTimestamp()) throw new WeixinException(HttpStatus.GATEWAY_TIMEOUT, "token 过期失效");
        return result;
    }

    /**
     * 通过 userAccessToken 获取 jwt
     * @param userAccessToken
     * @return
     */
    public JsonWebToken getJwtByUserAccessToken(UserAccessToken userAccessToken) {
        if (this.updateJwt(userAccessToken)) {
            try {
                return getJwtByToken(userAccessToken.getAccess_token());
            } catch (WeixinException e) {}
        }
        return createJwt(userAccessToken);
    }

    /**
     * 通过 token 获取 openid
     * @param token
     * @return
     */
    public String getOpenIdByToken(String token) throws WeixinException {
        Map result = jdbcTemplate.query(
                "select openid, `timestamp` from jwt where token = ?",
                preparedStatement -> {
                    preparedStatement.setString(1, token);
                },
                resultSet -> {
                    if (resultSet.next()) {
                        Map data = new HashMap();
                        data.put("openId", resultSet.getString("openid"));
                        data.put("timestamp", resultSet.getInt("timestamp"));
                        return data;
                    }
                    return null;
                }
        );
        if (result == null) throw new WeixinException(HttpStatus.NOT_FOUND, "token 失效");
        if ((int)result.get("timestamp") < getTimestamp()) throw new WeixinException(HttpStatus.GATEWAY_TIMEOUT, "token 过期失效");
        return (String)result.get("openId");
    }

    /**
     * 将 jwt 表单数据转化为对象
     * @param jwtFormData
     * @param clazz
     * @param <T>
     * @return
     * @throws WeixinException
     */
    public <T>T parseToObject(JwtFormData jwtFormData, Class<T> clazz) throws WeixinException {
        if (jwtFormData == null) throw new WeixinException(HttpStatus.BAD_REQUEST, "jwt 解析失败");
        String token = jwtFormData.getToken();
        String secret = this.getSecretByToken(token);
        String signature = jwtFormData.getSignature();
        String headers = jwtFormData.getHeaders();
        String payload = jwtFormData.getPayload();
        String checkStr = this.HMACSHA256(headers + "." + payload, secret);
        if (!checkStr.equalsIgnoreCase(signature))  throw new WeixinException(HttpStatus.BAD_REQUEST, "表单验证失败");
        Gson gson = new Gson();
        String data = new String(Base64.getDecoder().decode(payload));
        System.out.println(data);
        return gson.fromJson(data, clazz);
    }
    /**
     * 通过 userAccessToken 生成 jwt
     * @param userAccessToken
     * @return
     */
    private JsonWebToken createJwt(UserAccessToken userAccessToken) {
        int timestamp = (int)(System.currentTimeMillis()/1000) + userAccessToken.getExpires_in();
        String secret = this.createSecret(10);
        jdbcTemplate.update(
                "insert into jwt(openid, token, refresh_token, scope, `timestamp`, secret) values(?, ?, ?, ?, ?, ?)",
                preparedStatement -> {
                    preparedStatement.setString(1, userAccessToken.getOpenid());
                    preparedStatement.setString(2, userAccessToken.getAccess_token());
                    preparedStatement.setString(3, userAccessToken.getRefresh_token());
                    preparedStatement.setString(4, userAccessToken.getScope());
                    preparedStatement.setInt(5, timestamp);
                    preparedStatement.setString(6, secret);
                }
        );
        JsonWebToken jwt = new JsonWebToken();
        jwt.setToken(userAccessToken.getAccess_token());
        jwt.setRefreshToken(userAccessToken.getRefresh_token());
        jwt.setTimestamp(timestamp);
        jwt.setSecret(secret);
        return jwt;
    }

    /**
     * 通过 userAccessToken 更新 jwt
     * @param userAccessToken
     * @return
     */
    private boolean updateJwt(UserAccessToken userAccessToken) {
        int timestamp = (int)(System.currentTimeMillis()/1000) + userAccessToken.getExpires_in();
        int effectRow = jdbcTemplate.update(
                "update jwt set token = ?, refresh_token = ?, `timestamp` = ?, scope = ? where openid = ?",
                    preparedStatement -> {
                        preparedStatement.setString(1, userAccessToken.getAccess_token());
                        preparedStatement.setString(2, userAccessToken.getRefresh_token());
                        preparedStatement.setInt(3, timestamp);
                        preparedStatement.setString(4, userAccessToken.getScope());
                        preparedStatement.setString(5, userAccessToken.getOpenid());
                    }
                );
        return effectRow > 0;
    }

    private String createSecret(int length) {
        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        Random random = new Random();
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < length; i++) {
            buffer.append(str.charAt(random.nextInt(52)));
        }
        return buffer.toString();
    }

    private int getTimestamp() {
        return (int)(System.currentTimeMillis()/1000);
    }

    private String getSecretByToken(String token) throws WeixinException {
        JsonWebToken jsonWebToken = this.getJwtByToken(token);
        return jsonWebToken.getSecret();
    }

    private static String HMACSHA256(String data, String key) {
        try  {
            SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA256");
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(signingKey);
            byte[] b = mac.doFinal(data.getBytes());
            StringBuilder hs = new StringBuilder();
            String stmp;
            for (int n = 0; b!=null && n < b.length; n++) {
                stmp = Integer.toHexString(b[n] & 0XFF);
                if (stmp.length() == 1)
                    hs.append('0');
                hs.append(stmp);
            }
            return hs.toString().toUpperCase();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        }
        return null;
    }
}
